Skip to content

add SubjectAs/IssSub accessors for the sub_id pointer/value asymmetry#15

Merged
hstern merged 1 commit into
mainfrom
secevent-subject-accessor
Jun 21, 2026
Merged

add SubjectAs/IssSub accessors for the sub_id pointer/value asymmetry#15
hstern merged 1 commit into
mainfrom
secevent-subject-accessor

Conversation

@hstern

@hstern hstern commented Jun 21, 2026

Copy link
Copy Markdown
Owner

What

Adds an ergonomic, typed accessor over SET.Subject so consumers don't have to handle a value-vs-pointer dynamic-type asymmetry by hand.

  • SubjectAs[T subjectid.SubjectIdentifier](s *SET) (T, bool) — the general accessor.
  • (*SET) IssSub() (subjectid.IssSubID, bool) — non-generic shorthand for the common iss_sub case.
  • SET.Subject godoc now states the parsed dynamic type (pointer form).
  • Runnable ExampleSET_IssSub.

Why

SET.Subject is typed subjectid.SubjectIdentifier (an interface). Its dynamic type depends on how the SET was built:

  • Parsed from the wireParse delegates sub_id to subjectid.Parse, whose registry constructors return pointers, so set.Subject is *subjectid.IssSubID.
  • Built in Go — the concrete types satisfy the interface with value receivers, so the obvious literal is the value form subjectid.IssSubID{}.

A consumer therefore writes IssSubID{} but reads back *IssSubID; the obvious assertion set.Subject.(subjectid.IssSubID) silently takes the !ok branch after a parse and drops the event — a footgun that passes a hand-built unit test and fails on real wire input (or vice-versa). SubjectAs tries the value form first, then peels a single pointer indirection via reflection, returning the value either way (a type assertion to *T won't compile for an arbitrary type parameter; a nil-pointer guard keeps reflect.Value.Elem from panicking on a typed nil).

This is the non-breaking, consumer-side half. Normalizing the canonical form in go-subjectid's Parse is a wider, possibly breaking decision and is tracked separately.

Tests

Value form, pointer form, absent / nil-receiver / typed-nil / wrong-format misses, and the acceptance round-trip (SET{Subject: IssSubID{...}}EncodeParse → equal IssSubID via the accessor). No wire-format change.

🤖 Generated with Claude Code

SET.Subject is typed subjectid.SubjectIdentifier, an interface whose
dynamic type depends on how the SET was built. Parse delegates sub_id to
subjectid.Parse, whose registry constructors return pointers, so a parsed
SET carries the pointer form (*subjectid.IssSubID). A SET built in Go
naturally holds the value form (subjectid.IssSubID), because the concrete
types satisfy the interface with value receivers. The result is an
asymmetric round-trip: a consumer writes IssSubID{} but reads back
*IssSubID, and the obvious assertion set.Subject.(subjectid.IssSubID)
silently takes the !ok branch after a parse and drops the event. It is the
kind of bug that passes a hand-built unit test (value form) and fails on
real wire input (pointer form). A downstream Shared Signals consumer hit
exactly this.

SubjectAs[T] absorbs the distinction once for all consumers: it tries a
direct assertion to T (the value form) and, failing that, peels a single
pointer indirection via reflection to match the pointer form, returning
the value either way. A type assertion to *T will not compile for an
arbitrary type parameter, so the pointer case goes through reflect; the
nil-pointer guard keeps reflect.Value.Elem from panicking on a typed nil
in the interface. IssSub is the non-generic shorthand for the
overwhelmingly common iss_sub format. The SET.Subject godoc now states the
parsed dynamic type so the asymmetry is discoverable from godoc alone.

This is the non-breaking, consumer-side half of the fix. Normalizing the
canonical form in go-subjectid's Parse is a wider, possibly breaking
decision and is tracked separately.

Tests cover the value form, the pointer form, the absent / nil-receiver /
typed-nil / wrong-format misses, and the acceptance round-trip
(SET{Subject: IssSubID{...}} through Encode and Parse back to an equal
IssSubID via the accessor). A runnable ExampleSET_IssSub documents the
parse-then-read flow. No wire-format change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hstern hstern enabled auto-merge (squash) June 21, 2026 15:13
@hstern hstern merged commit bab57b5 into main Jun 21, 2026
4 checks passed
@hstern hstern deleted the secevent-subject-accessor branch June 21, 2026 15:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant